Découvrez les concepts clés et les techniques avancées du rendu d'ombres en temps réel en WebGL. Ce guide couvre le shadow mapping, PCF, CSM et les solutions aux artefacts courants.
WebGL Shadow Mapping : Un guide complet pour le rendu en temps réel
Dans le monde des graphiques 3D, peu d'éléments contribuent autant au réalisme et à l'immersion que les ombres. Elles fournissent des indications visuelles cruciales sur les relations spatiales entre les objets, l'emplacement des sources de lumière et la géométrie globale d'une scène. Sans ombres, les mondes 3D peuvent sembler plats, déconnectés et artificiels. Pour les applications 3D basées sur le web, alimentées par WebGL, la mise en œuvre d'ombres de haute qualité en temps réel est une marque distinctive des expériences de qualité professionnelle. Ce guide fournit une plongée en profondeur dans la technique la plus fondamentale et la plus largement utilisée pour y parvenir : le Shadow Mapping.
Que vous soyez un programmeur graphique chevronné ou un développeur web s'aventurant dans la troisième dimension, cet article vous dotera des connaissances nécessaires pour comprendre, implémenter et dépanner les ombres en temps réel dans vos projets WebGL. Nous voyagerons de la théorie de base aux détails pratiques de l'implémentation, en explorant les pièges courants et les techniques avancées utilisées dans les moteurs graphiques modernes.
Chapitre 1 : Les fondamentaux du Shadow Mapping
À la base, le shadow mapping est une technique ingénieuse et élégante qui détermine si un point dans une scène est dans l'ombre en posant une simple question : "Ce point peut-il être vu par la source de lumière ?" Si la réponse est non, cela signifie que quelque chose bloque la lumière et que le point doit être dans l'ombre. Pour répondre à cette question par programmation, nous utilisons une approche de rendu en deux passes.
Qu'est-ce que le Shadow Mapping ? Le concept de base
Toute la technique repose sur le rendu de la scène deux fois, à chaque fois d'un point de vue différent :
- Passage 1 : Le passage de la profondeur (La perspective de la lumière). Tout d'abord, nous rendons toute la scène à partir de la position et de l'orientation exactes de la source de lumière. Cependant, nous ne nous soucions pas des couleurs ou des textures dans ce passage. La seule information dont nous avons besoin est la profondeur. Pour chaque objet rendu, nous enregistrons sa distance par rapport à la source de lumière. Cet ensemble de valeurs de profondeur est stocké dans une texture spéciale appelée shadow map ou depth map. Chaque pixel de cette carte représente la distance par rapport à l'objet le plus proche du point de vue de la lumière dans une direction spécifique.
- Passage 2 : Le passage de la scène (La perspective de la caméra). Ensuite, nous rendons la scène comme nous le ferions normalement, du point de vue de la caméra principale. Mais pour chaque pixel dessiné, nous effectuons un calcul supplémentaire. Nous déterminons la position de ce pixel dans l'espace 3D, puis nous demandons : "À quelle distance ce point se trouve-t-il de la source de lumière ?" Nous comparons ensuite cette distance à la valeur stockée dans notre shadow map (du passage 1) à l'emplacement correspondant.
La logique est simple :
- Si la distance actuelle du pixel par rapport à la lumière est supérieure à la distance stockée dans la shadow map, cela signifie qu'il y a un autre objet plus proche de la lumière le long de cette même ligne de visée. Par conséquent, le pixel actuel est dans l'ombre.
- Si la distance du pixel est inférieure ou égale à la distance dans la shadow map, cela signifie que rien ne le bloque et que le pixel est entièrement éclairé.
Configuration de la scène
Pour implémenter le shadow mapping en WebGL, vous avez besoin de plusieurs composants clés :
- Une source de lumière : Il peut s'agir d'une lumière directionnelle (comme le soleil), d'une lumière ponctuelle (comme une ampoule) ou d'un spot. Le type de lumière déterminera le type de matrice de projection utilisée pendant le passage de la profondeur.
- Un objet de tampon de trame (FBO) : WebGL effectue normalement le rendu sur le framebuffer par défaut de l'écran. Pour créer notre shadow map, nous avons besoin d'une cible de rendu hors écran. Un FBO nous permet de rendre dans une texture au lieu de l'écran. Notre FBO sera configuré avec un attachement de texture de profondeur.
- Deux ensembles de shaders : Vous aurez besoin d'un programme de shader pour le passage de la profondeur (un très simple) et d'un autre pour le passage de la scène finale (qui contiendra la logique de calcul des ombres).
- Matrices : Vous aurez besoin des matrices standard de modèle, de vue et de projection pour la caméra. Il est crucial que vous ayez également une matrice de vue et de projection pour la source de lumière, souvent combinées en une seule "matrice d'espace lumière".
Chapitre 2 : Le pipeline de rendu en deux passes en détail
Décomposons les deux passages de rendu étape par étape, en nous concentrant sur les rôles des matrices et des shaders.
Passage 1 : Le passage de la profondeur (Du point de vue de la lumière)
L'objectif de ce passage est de remplir notre texture de profondeur. Voici comment cela fonctionne :
- Lier le FBO : Avant de dessiner, vous indiquez à WebGL de rendre sur votre FBO personnalisé au lieu du canevas.
- Configurer la fenêtre d'affichage : Définissez les dimensions de la fenêtre d'affichage pour qu'elles correspondent à la taille de votre texture de shadow map (par exemple, 1024x1024 pixels).
- Effacer le tampon de profondeur : Assurez-vous que le tampon de profondeur du FBO est effacé avant le rendu.
- Créer les matrices de la lumière :
- Matrice de vue de la lumière : Cette matrice transforme le monde dans le point de vue de la lumière. Pour une lumière directionnelle, celle-ci est généralement créée avec une fonction `lookAt`, où l'"œil" est la position de la lumière et la "cible" est la direction vers laquelle elle pointe.
- Matrice de projection de la lumière : Pour une lumière directionnelle, qui a des rayons parallèles, une projection orthographique est utilisée. Pour les lumières ponctuelles ou les spots, une projection perspective est utilisée. Cette matrice définit le volume dans l'espace (une boîte ou un frustum) qui projettera des ombres.
- Utiliser le programme de shader de profondeur : Il s'agit d'un shader minimal. Le seul rôle du vertex shader est de multiplier la position du vertex par les matrices de vue et de projection de la lumière. Le fragment shader est encore plus simple : il écrit simplement la valeur de profondeur du fragment (sa coordonnée z) dans la texture de profondeur. Dans WebGL moderne, vous n'avez souvent même pas besoin d'un fragment shader personnalisé, car le FBO peut être configuré pour capturer automatiquement le tampon de profondeur.
- Rendre la scène : Dessinez tous les objets projetant des ombres dans votre scène. Le FBO contient maintenant notre shadow map complétée.
Passage 2 : Le passage de la scène (Du point de vue de la caméra)
Nous rendons maintenant l'image finale, en utilisant la shadow map que nous venons de créer pour déterminer les ombres.
- Délier le FBO : Revenez au rendu sur le framebuffer du canevas par défaut.
- Configurer la fenêtre d'affichage : Définissez la fenêtre d'affichage sur les dimensions du canevas.
- Effacer l'écran : Effacez les tampons de couleur et de profondeur du canevas.
- Utiliser le programme de shader de la scène : C'est là que la magie opère. Ce shader est plus complexe.
- Vertex Shader : Ce shader doit faire deux choses. Premièrement, il calcule la position finale du vertex en utilisant les matrices de modèle, de vue et de projection de la caméra comme d'habitude. Deuxièmement, il doit également calculer la position du vertex à partir de la perspective de la lumière en utilisant la matrice d'espace lumière du passage 1. Cette deuxième coordonnée est passée au fragment shader comme une variable.
- Fragment Shader : C'est le cœur de la logique des ombres. Pour chaque fragment :
- Recevoir la position interpolée dans l'espace lumière du vertex shader.
- Effectuer une division de perspective sur cette coordonnée (diviser x, y, z par w). Cela la transforme en coordonnées de périphérique normalisées (NDC), allant de -1 à 1.
- Transformer le NDC en coordonnées de texture (qui vont de 0 à 1) afin que nous puissions échantillonner notre shadow map. Il s'agit d'une simple opération d'échelle et de biais : `texCoord = ndc * 0.5 + 0.5;`.
- Utiliser ces coordonnées de texture pour échantillonner la texture de shadow map créée au passage 1. Cela nous donne `depthFromShadowMap`.
- La profondeur actuelle du fragment du point de vue de la lumière est sa composante z à partir de la coordonnée transformée de l'espace lumière. Appelons-la `currentDepth`.
- Comparer les profondeurs : Si `currentDepth > depthFromShadowMap`, le fragment est dans l'ombre. Nous devrons ajouter un petit biais à cette vérification pour éviter un artefact appelé "acné d'ombre", dont nous discuterons ensuite.
- En fonction de la comparaison, déterminer un facteur d'ombre (par exemple, 1,0 pour éclairé, 0,3 pour ombré).
- Appliquer ce facteur d'ombre au calcul de la couleur finale (par exemple, multiplier les composantes d'éclairage ambiant et diffus par le facteur d'ombre).
- Rendre la scène : Dessinez tous les objets de la scène.
Chapitre 3 : Problèmes courants et solutions
La mise en œuvre du shadow mapping de base révélera rapidement plusieurs artefacts visuels courants. Les comprendre et les corriger est crucial pour obtenir des résultats de haute qualité.
Acné d'ombre (Artefacts d'auto-ombrage)
Le problème : Vous pouvez voir d'étranges motifs incorrects de lignes sombres ou des motifs de type Moiré sur les surfaces qui devraient être entièrement éclairées. C'est ce qu'on appelle "acné d'ombre". Cela se produit parce que la valeur de profondeur stockée dans la shadow map et la valeur de profondeur calculée pendant le passage de la scène sont pour la même surface. En raison des imprécisions en virgule flottante et de la résolution limitée de la shadow map, de minuscules erreurs peuvent amener un fragment à déterminer de manière incorrecte qu'il est derrière lui-même, ce qui entraîne un auto-ombrage.
La solution : Biais de profondeur. La solution la plus simple consiste à introduire un petit biais dans le `currentDepth` avant la comparaison. En faisant apparaître le fragment légèrement plus près de la lumière qu'il ne l'est réellement, nous le poussons "en dehors" de sa propre ombre.
float shadow = currentDepth > depthFromShadowMap + bias ? 0.3 : 1.0;
Trouver la bonne valeur de biais est un exercice d'équilibre délicat. Trop petit, et l'acné persiste. Trop grand, et vous obtenez le problème suivant.
Peter Panning
Le problème : Cet artefact, nommé d'après le personnage qui pouvait voler et perdait son ombre, se manifeste par un espace visible entre un objet et son ombre. Il donne l'impression que les objets flottent ou sont déconnectés des surfaces sur lesquelles ils devraient reposer. C'est le résultat direct de l'utilisation d'un biais de profondeur trop important.
La solution : Biais de profondeur d'échelle de pente. Une solution plus robuste qu'un biais constant consiste à rendre le biais dépendant de la raideur de la surface par rapport à la lumière. Les polygones les plus raides sont plus sujets à l'acné et nécessitent un biais plus important. Les polygones plus plats ont besoin d'un biais plus petit. La plupart des API graphiques, y compris WebGL, offrent des fonctionnalités pour appliquer automatiquement ce type de biais pendant le passage de la profondeur, ce qui est généralement préférable à un biais manuel dans le fragment shader.
Aliasing de perspective (bords en escalier)
Le problème : Les bords de vos ombres sont bloqués, dentelés et pixellisés. Il s'agit d'une forme d'aliasing. Cela se produit parce que la résolution de la shadow map est finie. Un seul pixel (ou texel) dans la shadow map peut couvrir une grande surface sur une surface dans la scène finale, en particulier pour les surfaces proches de la caméra ou celles vues sous un angle rasant. Cette inadéquation de résolution provoque l'apparence en blocs caractéristique.
La solution : L'augmentation de la résolution de la shadow map (par exemple, de 1024x1024 à 4096x4096) peut aider, mais elle a un coût important en termes de mémoire et de performances et ne résout pas complètement le problème sous-jacent. Les vraies solutions résident dans des techniques plus avancées.
Chapitre 4 : Techniques avancées de Shadow Mapping
Le shadow mapping de base fournit une base, mais les applications professionnelles utilisent des algorithmes plus sophistiqués pour surmonter ses limites, en particulier l'aliasing.
Filtrage par pourcentage plus proche (PCF)
Le PCF est la technique la plus courante pour adoucir les bords des ombres et réduire l'aliasing. Au lieu de prendre un seul échantillon à partir de la shadow map et de prendre une décision binaire (dans l'ombre ou non dans l'ombre), le PCF prend plusieurs échantillons à partir de la zone autour de la coordonnée cible.
Le concept : Pour chaque fragment, nous échantillonnons la shadow map non pas une seule fois, mais selon un motif de grille (par exemple, 3x3 ou 5x5) autour de la coordonnée de texture projetée du fragment. Pour chacun de ces échantillons, nous effectuons la comparaison de la profondeur. La valeur d'ombre finale est la moyenne de toutes ces comparaisons. Par exemple, si 4 des 9 échantillons sont dans l'ombre, le fragment sera ombré aux 4/9èmes, ce qui donnera une pénombre (le bord flou d'une ombre).
Mise en œuvre : Ceci se fait entièrement dans le fragment shader. Il implique une boucle qui itère sur un petit noyau, échantillonnant la shadow map à chaque décalage et accumulant les résultats. WebGL 2 offre une prise en charge matérielle (`texture` avec un `sampler2DShadow`) qui peut effectuer la comparaison et le filtrage plus efficacement.
Avantage : Améliore considérablement la qualité des ombres en remplaçant les bords durs et aliasés par des bords lisses et doux.
Coût : Les performances diminuent avec le nombre d'échantillons pris par fragment.
Cascaded Shadow Maps (CSM)
CSM est la solution standard de l'industrie pour le rendu des ombres à partir d'une seule source de lumière directionnelle (comme le soleil) sur une très grande scène. Il s'attaque directement au problème de l'aliasing de perspective.
Le concept : L'idée de base est que les objets proches de la caméra ont besoin d'une résolution d'ombre beaucoup plus élevée que les objets éloignés. CSM divise le frustum de la caméra en plusieurs sections, ou "cascades", le long de sa profondeur. Une shadow map distincte et de haute qualité est ensuite rendue pour chaque cascade. La cascade la plus proche de la caméra couvre une petite zone de l'espace mondial et a donc une très haute résolution effective. Les cascades plus éloignées couvrent des zones de plus en plus grandes avec la même taille de texture, ce qui est acceptable car ces détails sont moins visibles pour le joueur.
Mise en œuvre : Ceci est beaucoup plus complexe.
- Dans le processeur, divisez le frustum de la caméra en 2 à 4 cascades.
- Pour chaque cascade, calculez une matrice de projection orthographique ajustée pour la lumière qui englobe parfaitement cette section du frustum.
- Dans la boucle de rendu, effectuez le passage de la profondeur plusieurs fois, une fois pour chaque cascade, en rendant dans une shadow map différente (ou une région d'un atlas de texture).
- Dans le fragment shader du passage de la scène finale, déterminez à quelle cascade appartient le fragment actuel en fonction de sa distance par rapport à la caméra.
- Échantillonnez la shadow map de la cascade appropriée pour calculer l'ombre.
Avantage : Fournit des ombres de haute résolution de manière constante sur de vastes distances, ce qui le rend parfait pour les environnements extérieurs.
Variance Shadow Maps (VSM)
VSM est une autre technique pour créer des ombres douces, mais elle adopte une approche différente du PCF.
Le concept : Au lieu de stocker uniquement la profondeur dans la shadow map, VSM stocke deux valeurs : la profondeur (le premier moment) et la profondeur au carré (le second moment). Ces deux valeurs nous permettent de calculer la variance de la distribution de la profondeur. En utilisant un outil mathématique appelé l'inégalité de Chebyshev, nous pouvons ensuite estimer la probabilité qu'un fragment soit dans l'ombre. Le principal avantage est qu'une texture VSM peut être floutée à l'aide d'un filtrage linéaire accéléré par le matériel standard et du mipmapping, ce qui est mathématiquement invalide pour une depth map standard. Cela permet des pénombres d'ombres très larges, douces et lisses avec un coût de performance fixe.
Inconvénient : La principale faiblesse de VSM est la "fuite de lumière", où la lumière peut sembler traverser les objets dans les situations avec des occluders qui se chevauchent, car l'approximation statistique peut s'effondrer.
Chapitre 5 : Conseils pratiques de mise en œuvre et performances
Choisir la résolution de votre shadow map
La résolution de votre shadow map est un compromis direct entre la qualité et les performances. Une texture plus grande offre des ombres plus nettes, mais consomme plus de mémoire vidéo et prend plus de temps à rendre et à échantillonner. Les tailles courantes incluent :
- 1024x1024 : Une bonne base pour de nombreuses applications.
- 2048x2048 : Offre une amélioration notable de la qualité pour les applications de bureau.
- 4096x4096 : Haute qualité, souvent utilisé pour les ressources principales ou dans les moteurs avec un culling robuste.
Optimisation du frustum de la lumière
Pour tirer le meilleur parti de chaque pixel de votre shadow map, il est crucial que le volume de projection de la lumière (sa boîte orthographique ou son frustum de perspective) soit aussi étroitement adapté que possible aux éléments de la scène qui ont besoin d'ombres. Pour une lumière directionnelle, cela signifie adapter sa projection orthographique pour n'englober que la partie visible du frustum de la caméra. Tout espace gaspillé dans la shadow map est une résolution gaspillée.
Extensions et versions WebGL
WebGL 1 vs. WebGL 2 : Bien que le shadow mapping soit possible dans WebGL 1, il est beaucoup plus facile et plus efficace dans WebGL 2. WebGL 1 nécessite l'extension `WEBGL_depth_texture` pour créer une texture de profondeur. WebGL 2 a cette fonctionnalité intégrée. De plus, WebGL 2 donne accès aux échantillonneurs d'ombres (`sampler2DShadow`), qui peuvent effectuer le PCF accéléré par le matériel, offrant une amélioration significative des performances par rapport aux boucles PCF manuelles dans le shader.
Débogage des ombres
Les ombres peuvent être notoirement difficiles à déboguer. La technique la plus utile est de visualiser la shadow map. Modifiez temporairement votre application pour rendre la texture de profondeur à partir d'une source de lumière spécifique directement sur un quad sur l'écran. Cela vous permet de voir exactement ce que la lumière "voit". Cela peut immédiatement révéler des problèmes avec les matrices de votre lumière, le culling du frustum ou le rendu des objets pendant le passage de la profondeur.
Conclusion
Le shadow mapping en temps réel est une pierre angulaire des graphiques 3D modernes, transformant les scènes plates et sans vie en mondes crédibles et dynamiques. Bien que le concept de rendu du point de vue d'une lumière soit simple, obtenir des résultats de haute qualité et sans artefacts nécessite une compréhension approfondie des mécanismes sous-jacents, du pipeline en deux passes aux nuances du biais de profondeur et de l'aliasing.
En commençant par une implémentation de base, vous pouvez progressivement vous attaquer aux artefacts courants comme l'acné d'ombre et les bords dentelés. À partir de là, vous pouvez améliorer vos visuels avec des techniques avancées comme le PCF pour les ombres douces ou les Cascaded Shadow Maps pour les environnements à grande échelle. Le voyage dans le rendu des ombres est un parfait exemple du mélange d'art et de science qui rend les graphiques informatiques si convaincants. Nous vous encourageons à expérimenter ces techniques, à repousser leurs limites et à apporter un nouveau niveau de réalisme à vos projets WebGL.